/**
 * @file cm_video_impl_v4l2_normal.c
 * @author jmdvirus
 * @date 2021-06-29 10:30:33
 */

#include <errno.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <poll.h>

#include "cm_common.h"
#include "cm_utils.h"
#include "rt_print.h"
#include "cm_video_ctrl.h"

typedef struct
{
    char         dev[128];
    int          fd;
    unsigned int width;
    unsigned int height;
    uint8_t      fps;
    int          buf_type;   // V4L2_BUF_TYPE_VIDEO_XXXX
    int          pix_format; // V4L2_PIX_FMT_xxx
    unsigned int n_buffers;
    CMVideoBuf  *buf;
} CMVideoPrivateData;

static int cm_video_impl_normal_stop(CMVideoContext *ctx);

static int cm_video_impl_normal_open(CMVideoContext *ctx, const char *dev)
{
    if (!ctx || !dev)
        return CM_E_ARGS;

    int fd = open(dev, O_RDWR | O_NONBLOCK);
    if (fd <= 0)
    {
        rterror("open failed [%s] dev [%s]", strerror(errno), dev);
        return -2;
    }

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)calloc(1, sizeof(CMVideoPrivateData));
    if (!_priv)
        return CM_E_NO_MEM;

    strncpy(_priv->dev, dev, sizeof(_priv->dev) - 1);

    _priv->fd         = fd;
    _priv->buf_type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    _priv->pix_format = V4L2_PIX_FMT_YUYV;
    _priv->fps        = 25;

    struct v4l2_fmtdesc fmtdesc;
    memset(&fmtdesc, 0, sizeof(fmtdesc));
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
        // 打印或处理每个格式
        rtdebug("fmt desc [%d] [%x] [%s]", fmtdesc.index, fmtdesc.pixelformat,
                fmtdesc.description);
        struct v4l2_frmsizeenum frmsize;
        memset(&frmsize, 0, sizeof(frmsize));
        frmsize.pixel_format = fmtdesc.pixelformat; // 使用VIDIOC_ENUM_FMT得到的格式
        while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) {
            // 打印或处理每个分辨率
            rtdebug("fmt size [%d] [%d x %d]", frmsize.index, frmsize.discrete.width,
                    frmsize.discrete.height);
            frmsize.index++;
        }

        fmtdesc.index++;
    }

    ctx->priv_data = _priv;
    rtdebug("video open dev [%s] success [%d] [%p]", dev, fd, ctx->priv_data);

    return CM_E_OK;
}

static int cm_video_impl_normal_close(CMVideoContext *ctx)
{
    if (!ctx || !ctx->priv_data)
        return CM_E_ARGS;

    cm_video_impl_normal_stop(ctx);

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (_priv->fd > 0)
    {
        int ret = close(_priv->fd);
        rtdebug("close fd [%d] ret [%d]", _priv->fd, ret);
        _priv->fd = 0;
    }
    free(_priv);

    ctx->priv_data = NULL;
    return CM_E_OK;
}

static int cm_vidoe_impl_normal_ctrl(CMVideoContext *ctx, int cmd, void *args)
{
    if (!ctx || cmd <= 0 || ctx->priv_data == NULL)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;

    switch (cmd)
    {
    case CM_VIDEO_CMD_SET_WIDTH:
        _priv->width = *(unsigned int *)args;
        break;
    case CM_VIDEO_CMD_SET_HEIGHT:
        _priv->height = *(unsigned int *)args;
        break;
    case CM_VIDEO_CMD_SET_FPS:
        _priv->fps = *(uint8_t *)args;
        break;
    case CM_VIDEO_CMD_SET_BUF_TYPE:
        _priv->buf_type = *(int *)args;
        break;
    case CM_VIDEO_CMD_SET_PIX_FORMAT:
    {
        int pix_format = *(int *)args;
        if (pix_format == CM_VIDEO_FORMAT_YUYV)
        {
            _priv->pix_format = V4L2_PIX_FMT_YUYV;
        }
        else if (pix_format == CM_VIDEO_FORMAT_JPEG)
        {
            _priv->pix_format = V4L2_PIX_FMT_MJPEG;
        }
        else if (pix_format == CM_VIDEO_FORMAT_H264)
        {
            _priv->pix_format = V4L2_PIX_FMT_H264;
        }
        else if (pix_format == CM_VIDEO_FORMAT_H265)
        {
            _priv->pix_format = V4L2_PIX_FMT_HEVC;
        }
        else
        {
            return CM_E_ARGS;
        }
    }
    break;
    case CM_VIDEO_CMD_SET_KEY_FRAME:
        rtdebug("request key frame fd [%d]", _priv->fd);
        if (_priv->fd)
        {
            struct v4l2_control control;
            control.value = 1;
            control.id    = V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME;
            ioctl(_priv->fd, VIDIOC_S_CTRL, &control);
        }
        break;
    }

    return CM_E_OK;
}

static int cm_video_impl_normal_set_stream(CMVideoContext *ctx)
{
    if (!ctx || ctx->priv_data == NULL)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (_priv->fd <= 0)
        return -2;

    struct v4l2_streamparm stream;
    stream.type                                  = _priv->buf_type;
    stream.parm.capture.capability               = V4L2_CAP_TIMEPERFRAME;
    stream.parm.capture.timeperframe.denominator = _priv->fps;
    stream.parm.capture.timeperframe.numerator   = 1;

    int ret = ioctl(_priv->fd, VIDIOC_S_PARM, &stream);
    if (ret < 0)
    {
        return -3;
    }

    return CM_E_OK;
}

static int cm_video_impl_normal_set_format(CMVideoContext *ctx)
{
    if (!ctx || ctx->priv_data == NULL)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (_priv->fd <= 0)
        return -2;

    struct v4l2_format format;
    format.type                = _priv->buf_type;
    format.fmt.pix.width       = _priv->width;
    format.fmt.pix.height      = _priv->height;
    format.fmt.pix.field       = V4L2_FIELD_INTERLACED;
    format.fmt.pix.pixelformat = _priv->pix_format;

    int ret = ioctl(_priv->fd, VIDIOC_S_FMT, &format);
    if (ret < 0)
    {
        return -3;
    }

    ret = ioctl(_priv->fd, VIDIOC_G_FMT, &format);
    if (ret < 0)
    {
        return -4;
    }

    char fmtstr[8] = {0};
    memcpy(fmtstr, &format.fmt.pix.pixelformat, 4);
    rtdebug("video format is [%s] [%d x %d]", fmtstr, format.fmt.pix.width,
            format.fmt.pix.height);

    return CM_E_OK;
}

static int cm_video_impl_normal_queue(CMVideoContext *ctx)
{
    if (!ctx || ctx->priv_data == NULL)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (_priv->fd <= 0)
        return -2;

    struct v4l2_requestbuffers req;
    struct v4l2_buffer         buf;

    req.count  = 4;
    req.type   = _priv->buf_type;
    req.memory = V4L2_MEMORY_MMAP;

    int ret = ioctl(_priv->fd, VIDIOC_REQBUFS, &req);
    if (ret < 0)
    {
        return -3;
    }

    _priv->buf = (CMVideoBuf *)calloc(req.count, sizeof(CMVideoBuf));
    if (!_priv->buf)
        return CM_E_NO_MEM;

    int n_buffers = 0;
    for (n_buffers = 0; n_buffers < req.count; n_buffers++)
    {
        buf.type   = _priv->buf_type;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = n_buffers;
        ret        = ioctl(_priv->fd, VIDIOC_QUERYBUF, &buf);
        if (ret < 0)
        {
            rterror("video querybuf index [%d] fail [%d] [%s]", n_buffers, ret, strerror(errno));
            continue;
        }
        _priv->buf[n_buffers].length = buf.length;
        _priv->buf[n_buffers].start =
            mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, _priv->fd, buf.m.offset);
    }
    _priv->n_buffers = n_buffers;

    for (int i = 0; i < n_buffers; i++)
    {
        buf.type   = _priv->buf_type;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;
        ret        = ioctl(_priv->fd, VIDIOC_QBUF, &buf);
        if (ret < 0)
        {
            rterror("video QBUF index [%d] fail [%d] [%s]", i, ret, strerror(errno));
            continue;
        }
    }

    return CM_E_OK;
}

static int cm_video_impl_normal_start(CMVideoContext *ctx)
{
    if (!ctx)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (!_priv || _priv->fd <= 0)
        return -2;

    int ret = cm_video_impl_normal_set_stream(ctx);
    if (ret < 0)
    {
        rterror("video set_stream fail [%d] [%s]", ret, strerror(errno));
        return -2;
    }
    ret = cm_video_impl_normal_set_format(ctx);
    if (ret < 0)
    {
        rterror("video set format fail [%d] [%s]", ret, strerror(errno));
        return -3;
    }

    ret = cm_video_impl_normal_queue(ctx);
    if (ret < 0)
    {
        rterror("video set queue fail [%d] [%s]", ret, strerror(errno));
        return -4;
    }

    int type = _priv->buf_type;
    ret      = ioctl(_priv->fd, VIDIOC_STREAMON, &type);
    if (ret < 0)
    {
        rterror("video stream on fail [%d] [%s]", ret, strerror(errno));
        return -5;
    }

    return CM_E_OK;
}

static int cm_video_impl_normal_stop(CMVideoContext *ctx)
{
    if (!ctx)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (!_priv || _priv->fd <= 0)
        return -2;

    rtdebug("video munmap buffers count [%d]", _priv->n_buffers);
    for (int i = 0; i < _priv->n_buffers; i++)
    {
        munmap(_priv->buf[i].start, _priv->buf[i].length);
    }
    if (_priv->buf)
    {
        free(_priv->buf);
        _priv->buf = NULL;
    }
    _priv->n_buffers = 0;

    return CM_E_OK;
}

static int cm_video_impl_normal_read_data(CMVideoPrivateData *priv, CMVideoBuf *vbuf)
{
    if (!priv || priv->fd <= 0 || !vbuf)
        return CM_E_ARGS;

    struct v4l2_buffer     buf;
    struct v4l2_capability cap;

    memset(&buf, 0, sizeof(struct v4l2_buffer));
    buf.type   = priv->buf_type;
    buf.memory = V4L2_MEMORY_MMAP;
    int ret    = ioctl(priv->fd, VIDIOC_DQBUF, &buf);
    if (ret < 0)
        return -2;
    ret = ioctl(priv->fd, VIDIOC_QUERYCAP, &cap);
    if (ret < 0)
        return -3;

    // 数据成功获取
    vbuf->length = buf.bytesused;
    
    vbuf->start = priv->buf[buf.index].start;

    ret = ioctl(priv->fd, VIDIOC_QBUF, &buf);
    if (ret < 0)
    {
        rterror("video qbuf fail [%d] [%s]", ret, strerror(errno));
        return -4;
    }
    return CM_E_OK;
}

static int cm_video_impl_normal_read(CMVideoContext *ctx, CMVideoBuf *buf)
{
    if (!ctx)
        return CM_E_ARGS;

    CMVideoPrivateData *_priv = (CMVideoPrivateData *)ctx->priv_data;
    if (!_priv || _priv->fd <= 0)
        return -2;

    struct pollfd pfd[1];
    pfd[0].fd = _priv->fd;
    pfd[0].events = POLLIN | POLLERR | POLLRDNORM;
    pfd[0].revents = 0;
    int ret        = poll(pfd, 1, 40);
    if (ret < 0)
    {
        rterror("video poll error [%d]", ret);
        return -3;
    }
    // 有数据
    if (pfd[0].revents & POLLIN)
    {
        ret = cm_video_impl_normal_read_data(_priv, buf);
        if (ret < 0)
        {
            rterror("video read data fail [%d]", ret);
            return -4;
        }
        return CM_E_DONE;
    }
    else if (pfd[0].revents & POLLERR)
    {
        rterror("video poll has error result, may be has closed");
        return -5;
    }
    else
    {
        //        rtdebug("poll events is [%02x] ret [%d]",
        //        pfd[0].revents, ret);
    }

    return CM_E_OK;
}

const CMVideoImpl cm_video_impl_v4l2 = {
    .name  = "v4l2-normal",
    .open  = cm_video_impl_normal_open,
    .close = cm_video_impl_normal_close,
    .ctrl  = cm_vidoe_impl_normal_ctrl,
    .start = cm_video_impl_normal_start,
    .stop  = cm_video_impl_normal_stop,
    .read  = cm_video_impl_normal_read,
};
